﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using AmiEmuLib.AmigaConfiguration;
using AmiEmuLib.AmigaConfiguration.MemoryConfig;
using System.IO;
using System.Collections;
using AmiEmuLib.Configuration.AmigaMemoryMap;
using TomanuExtensions.Utils;

namespace AmiEmuLib.AmigaMachine.AmigaMemory
{
    /// <summary>
    /// Abstract memory model.
    /// </summary>
    public class Memory : IEnumerable<MemoryBank>
    {
        /// <summary>
        /// Machine to which this memory belong.
        /// </summary>
        public readonly Machine Machine;

        /// <summary>
        /// 64K banks, each of 64KB size, which gives 4GB address space.
        /// </summary>
        private MemoryBank[] m_banks = new MemoryBank[Consts.BANKS_COUNT];

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="a_machine"></param>
        /// <param name="a_map"></param>
        public Memory(Machine a_machine)
        {
            Machine = a_machine;
            a_machine.OnReset += Reset;

            var map = MemoryMaps.GetMap(a_machine.Config.AmigaModel);

            if (Machine.Config.CPU.AddressSpace24Bits)
            {
                for (uint i = 0; i < Consts.BANKS_COUNT_24; i++)
                    SetBank(new VoidMemoryBank(i * Consts.BANK_SIZE, this));
            }
            else
            {
                for (uint i = 0; i < Consts.BANKS_COUNT; i++)
                    SetBank(new VoidMemoryBank(i * Consts.BANK_SIZE, this));
            }

            foreach (var map_bank in map.Chunks)
            {
                foreach (var bank in CreateBanks(map_bank))
                    SetBank(bank);
            }
        }

        /// <summary>
        /// Return all banks except mirrored and Void.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<MemoryBank> GetValidBanks()
        {
            if (Machine.Config.CPU.AddressSpace24Bits)
            {
                for (int i = 0; i < Consts.BANKS_COUNT_24; i++)
                {
                    if (!(m_banks[i] is VoidMemoryBank))
                        if (!(m_banks[i] is MirrorMemoryBank))
                            yield return m_banks[i];
                }
            }
            else
            {
                for (int i = 0; i < Consts.BANKS_COUNT; i++)
                {
                    if (!(m_banks[i] is VoidMemoryBank))
                        if (!(m_banks[i] is MirrorMemoryBank))
                            yield return m_banks[i];
                }
            }
        }

        /// <summary>
        /// Fills memory area.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_size"></param>
        /// <param name="a_value"></param>
        public void FillBytes(uint a_address, uint a_size, byte a_value)
        {
            while (a_address % 4 != 0)
            {
                SetByte(a_address, a_value);
                a_address++;
                a_size--;
            }

            uint long_sizes = (a_size / 4) * 4;
            FillLongs(a_address, long_sizes, MemoryHelpers.MakeLong(a_value, a_value, a_value, a_value));
            a_address += long_sizes;
            a_size -= long_sizes;

            while (a_size % 4 != 0)
            {
                SetByte(a_address, a_value);
                a_address++;
                a_size--;
            }
        }

        /// <summary>
        /// Fills memory area.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_size"></param>
        /// <param name="a_value"></param>
        public void FillWords(uint a_address, uint a_size, ushort a_value)
        {
            if (!MemoryHelpers.IsWordAllign(a_address))
                throw new ArgumentException();
            if (!MemoryHelpers.IsWordAllign(a_size))
                throw new ArgumentException();

            if (a_size == 0)
                return;

            while (a_address % 4 != 0)
            {
                SetWord(a_address, a_value);
                a_address += 2;
                a_size -= 2;
            }

            uint long_sizes = (a_size / 4) * 4;
            FillLongs(a_address, long_sizes, MemoryHelpers.MakeLong(a_value, a_value));
            a_address += long_sizes;
            a_size -= long_sizes;

            while (a_size > 0)
            {
                SetWord(a_address, a_value);
                a_address += 2;
                a_size -= 2;
            }
        }

        /// <summary>
        /// Fills memory area.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_size"></param>
        /// <param name="a_value"></param>
        public void FillLongs(uint a_address, uint a_size, uint a_value)
        {
            if (!MemoryHelpers.IsLongAllign(a_address))
                throw new InvalidOperationException();
            if (!MemoryHelpers.IsLongAllign(a_size))
                throw new InvalidOperationException();

            if (a_size == 0)
                return;

            for (uint addr = a_address; addr < a_address + a_size; addr += 4)
                SetLong(addr, a_value);
        }

        /// <summary>
        /// Restore memmory to power-on state.
        /// </summary>
        private void Reset()
        {
            foreach (var bank in GetValidBanks())
                bank.Reset();
        }

        /// <summary>
        /// Get bank index from address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        private uint GetBankIndex(uint a_address)
        {
            return a_address / Consts.BANK_SIZE;
        }

        /// <summary>
        /// Get memory byte from address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public byte GetByte(uint a_address)
        {
            return m_banks[GetBankIndex(a_address)].GetByte(a_address);
        }

        /// <summary>
        /// Set memory byte in address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_value"></param>
        public void SetByte(uint a_address, byte a_value)
        {
            m_banks[GetBankIndex(a_address)].SetByte(a_address, a_value);
        }

        /// <summary>
        /// Get memory word from address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public ushort GetWord(uint a_address)
        {
            return m_banks[GetBankIndex(a_address)].GetWord(a_address);
        }

        /// <summary>
        /// Set memory word in address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_value"></param>
        public void SetWord(uint a_address, ushort a_value)
        {
            m_banks[GetBankIndex(a_address)].SetWord(a_address, a_value);
        }

        /// <summary>
        /// Get memory long from address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public uint GetLong(uint a_address)
        {
            return m_banks[GetBankIndex(a_address)].GetLong(a_address);
        }

        /// <summary>
        /// Set memory long in address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <param name="a_value"></param>
        public void SetLong(uint a_address, uint a_value)
        {
            m_banks[GetBankIndex(a_address)].SetLong(a_address, a_value);
        }

        /// <summary>
        /// Return memory bank from index.
        /// </summary>
        /// <param name="a_index"></param>
        /// <returns></returns>
        public MemoryBank this[uint a_index]
        {
            get
            {
                return m_banks[a_index];
            }
        }

        /// <summary>
        /// Set memory bank. 
        /// </summary>
        /// <param name="a_bank"></param>
        private void SetBank(MemoryBank a_bank)
        {
            uint index = GetBankIndex(a_bank.Address);

            if (Machine.Config.CPU.AddressSpace24Bits)
            {
                index = index % Consts.BANKS_COUNT_24;

                for (uint i = 0; i < Consts.BANKS_COUNT; i += Consts.BANKS_COUNT_24)
                {
                    m_banks[index] = a_bank;
                    index += Consts.BANKS_COUNT_24;
                }
            }
            else
                m_banks[index] = a_bank;
        }

        IEnumerator<MemoryBank> IEnumerable<MemoryBank>.GetEnumerator()
        {
            foreach (var bank in m_banks)
                yield return bank;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return m_banks.GetEnumerator();
        }

        /// <summary>
        /// Get bank from address.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public MemoryBank GetBank(uint a_address)
        {
            return m_banks[GetBankIndex(a_address)];
        }

        /// <summary>
        /// Create memory bank from memory map bank.
        /// </summary>
        /// <param name="a_chunk"></param>
        /// <param name="a_config"></param>
        /// <param name="a_memory"></param>
        /// <returns></returns>
        public IEnumerable<MemoryBank> CreateBanks(MemoryMapChunk a_chunk)
        {
            switch (a_chunk.Type)
            {
                case MemoryBankType.Chip: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.ChipMemorySize, true, false);
                case MemoryBankType.Fast: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.FastMemorySize, true, true);
                case MemoryBankType.Kickstart: return CreateKickstartBanks(a_chunk,
                    Machine.Config.Memory.Kickstart);
                case MemoryBankType.KickstartExtended: return CreateKickstartBanks(a_chunk,
                    Machine.Config.Memory.KickstartExtended);
                case MemoryBankType.Motherboard: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.MotherboardMemorySize, false, true);
                case MemoryBankType.ProcessorSlot: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.ProcessorSlotMemorySize, true, true);
                case MemoryBankType.Slow: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.SlowMemorySize, true,
                    Machine.Config.Chipset.SlowMemoryIsFast);
                case MemoryBankType.Z3: return CreateSimpleBanks(a_chunk,
                    Machine.Config.Memory.Z3MemorySize, true, true);
                default:
                    throw new InvalidOperationException();
            }
        }

        /// <summary>
        /// Create rom memory banks.
        /// </summary>
        /// <param name="a_chunk"></param>
        /// <param name="a_rom_info"></param>
        /// <returns></returns>
        private IEnumerable<MemoryBank> CreateKickstartBanks(MemoryMapChunk a_chunk, RomInfo a_rom_info)
        {
            if (!a_rom_info.Required)
                yield break;

            byte[] rom;

            try
            {
                string file_path = a_rom_info.FindRom();
                FileStream fs = new FileInfo(file_path).OpenRead();

                rom = FileUtils.ReadFile(file_path);
            }
            catch (Exception ex)
            {
                throw new RomLoadException(ex);
            }

            if (rom.Length == 0)
                yield break;

            if (rom.Length < Consts.BANK_SIZE)
            {
                // A1000 8KB loader rom.
                yield return new ReadOnlyMemoryBank(a_chunk.Address, this, a_chunk.Type, 
                                                    false, rom, 0);
            }
            else
            {
                uint address = a_chunk.Address + a_chunk.Size - (uint)rom.Length;
                for (ulong addr = address; addr < address + (uint)rom.Length; addr += Consts.BANK_SIZE)
                {
                    yield return new ReadOnlyMemoryBank((uint)addr, this, a_chunk.Type,
                                                        false, rom, (uint)addr - address);
                }
            }
        }

        /// <summary>
        /// Create simple memory banks.
        /// </summary>
        /// <param name="a_chunk"></param>
        /// <param name="a_size"></param>
        /// <param name="a_bottom_allign"></param>
        /// <param name="a_fast"></param>
        /// <returns></returns>
        private IEnumerable<MemoryBank> CreateSimpleBanks(MemoryMapChunk a_chunk, uint a_size, 
                                                          bool a_bottom_allign, bool a_fast)
        {
            uint start_addresss = a_chunk.Address;
            if (!a_bottom_allign)
                start_addresss += a_chunk.Size - a_size;

            if (a_size == 0)
                yield break;

            for (ulong addr = start_addresss; addr < start_addresss + a_size - 1;
                 addr += Consts.BANK_SIZE)
            {
                yield return new RamMemoryBank((uint)addr, this, a_chunk.Type, a_fast);
            }
        }
    }
}
